package Utils.io;

import Utils.basicDataTypes.TreeNode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;

import java.io.*;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;

import static Utils.data.StringUtils.repeatString;

/**
 * IO工具类（静态）
 *
 * @author d11yu
 */
@Slf4j
public final class IOUtils {
    //region getFileTree

    /**
     * @param path 根目录路径
     * @return 返回文件树
     */
    public static TreeNode<File> getFileTree(String path) {
        return getFileTree(new File(path));
    }

    /**
     * 获取文件树
     *
     * @param file 根目录文件
     * @return 返回文件树
     */
    public static TreeNode<File> getFileTree(File file) {
        TreeNode<File> node = new TreeNode<>(file);
        addFiles(node, file);
        return node;
    }

    /**
     * 如果是文件夹，则递归添加其子文件（夹）。
     *
     * @param node     FileTreeNode对象
     * @param rootFile 根文件（夹）
     */
    private static void addFiles(TreeNode<File> node, File rootFile) {
        // 如果是文件夹，则递归添加其子文件（夹）。
        if (rootFile.isDirectory()) {
            File[] files = rootFile.listFiles();
            // 用于记录成功添加进children的序号。
            int index = 0;
            for (File file : files) {
                node.add(file);
                addFiles(node.children.get(index), file);
                index++;
            }
        }
    }
    //endregion

    //region print file tree

    /**
     * 打印文件树
     *
     * @param file 根目录文件（夹）
     * @throws IOException
     */
    public static void printFiles(File file) throws IOException {
        printFiles(file, "    ");
    }

    /**
     * 打印文件树
     *
     * @param file   根目录文件（夹）
     * @param prefix 每行前缀重复字符串，默认为“    ”（4个空格）
     * @throws IOException
     */
    public static void printFiles(File file, String prefix) throws IOException {
        TreeNode<File> node = IOUtils.getFileTree(file);
        printFiles_(node, prefix);
    }

    /**
     * 打印文件树
     *
     * @param path 根目录路径
     */
    public static void printFiles(String path) throws IOException {
        printFiles(path, "    ");
    }

    /**
     * 打印文件树
     *
     * @param path   根目录路径
     * @param prefix 每行前缀重复字符串，默认为“    ”（4个空格）。
     */
    public static void printFiles(String path, String prefix) throws IOException {
        TreeNode<File> node = IOUtils.getFileTree(path);
        printFiles_(node, prefix);
    }

    private static void printFiles_(TreeNode<File> node, String prefix) throws IOException {
        if (node != null) {
            System.out.println(repeatString(prefix, node.level) + (node.data.isDirectory() ? node.data.getName() + "/" : node.data.getName()));
            if (node.children.size() > 0) {
                for (TreeNode<File> fileNode : node.children) {
                    System.out.println(repeatString("    ", fileNode.level) + (fileNode.data.isDirectory() ? fileNode.data.getName() + "/" : fileNode.data.getName()));
                    if (fileNode.data.isDirectory()) {
                        printFiles_(fileNode, prefix);
                    }
                }
            }
        }
    }
    //endregion

    //region Read file to string

    /**
     * 读取文件到字符串
     *
     * @param is 输入流
     * @return 返回字符串
     */
    public static String inputStream2String(InputStream is) throws IOException {
        return inputStream2String(is, StandardCharsets.UTF_8);
    }

    /**
     * 读取文件到字符串
     *
     * @param is      输入流
     * @param charset 编码，默认为utf-8
     * @return 返回字符串
     */
    public static String inputStream2String(InputStream is, Charset charset) throws IOException {
        try (ByteArrayOutputStream baos = inputStream2ByteArrayOutputStream(is)) {
            return baos.toString(String.valueOf(charset));
        }
    }

    /**
     * 读取文件到字符串
     *
     * @param path 文件路径
     * @return 返回字符串
     * @throws IOException
     */
    public static String file2String(String path) throws IOException {
        try (FileInputStream is = new FileInputStream(path)) {
            return inputStream2String(is);
        }
    }

    /**
     * 读取文件到字符串
     *
     * @param path    文件路径
     * @param charset 编码，默认为utf-8
     * @return 返回字符串
     * @throws IOException
     */
    public static String file2String(String path, Charset charset) throws IOException {
//        return Files.readString(Paths.get(path), charset); // JDK11及以上
        return new String(Files.readAllBytes(Paths.get(path)), charset);
    }
    //endregion

    //region Copy file

    /**
     * 复制文件到指定路径，适用于复制一般大小的文件。
     *
     * @param source 源文件路径
     * @param dest   目标文件路径
     * @throws IOException
     */
    public static void copyFile(String source, String dest) throws IOException {
        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {
//            in.transferTo(out); // JDK9及以上
            inputStream2OutputStream(in, out);
        }
    }

    /**
     * 复制文件
     *
     * @param source       源文件
     * @param dest         源文件
     * @param preserveDate 保留源文件的修改日期不变
     * @throws IOException
     */
    public static void copyFile_CommonsIO(String source, String dest, boolean preserveDate) throws IOException {
        copyFile_CommonsIO(new File(source), new File(dest), preserveDate);
    }

    /**
     * 复制文件
     *
     * @param source       源文件
     * @param dest         目标文件
     * @param preserveDate 保留源文件的修改日期不变
     * @throws IOException
     */
    public static void copyFile_CommonsIO(File source, File dest, boolean preserveDate) throws IOException {
        FileUtils.copyFile(source, dest, preserveDate);
    }

    /**
     * 复制文件到指定目录（不改变文件名），适用于复制一般大小的文件。
     *
     * @param source  源文件路径
     * @param destDir 目标文件目录directory
     * @throws IOException
     */
    public static void copyFile_SameName(String source, String destDir) throws IOException {
        String destName = new File(source).getName();
        String dest = Paths.get(destDir, destName).toString();
        try (FileInputStream in = new FileInputStream(source);
             FileOutputStream out = new FileOutputStream(dest)) {
//            in.transferTo(out); // JDK9及以上
            inputStream2OutputStream(in, out);
        }
    }

    /**
     * 复制文件到指定目录（不改变文件名），适用于复制大文件。
     *
     * @param source  源文件路径
     * @param destDir 目标文件目录directory
     * @throws IOException
     */
    public static void copyLargeFile_SameName(String source, String destDir) throws IOException {
        String destName = new File(source).getName();
        String dest = Paths.get(destDir, destName).toString();
        try (FileChannel in = new FileInputStream(source).getChannel();
             FileChannel out = new FileOutputStream(dest).getChannel()) {
            in.transferTo(0, in.size(), out);
//            out.transferFrom(in, 0, in.size());
        }
    }

    /**
     * 复制文件到指定路径，适用于复制大文件。
     *
     * @param source 源文件路径
     * @param dest   目标文件路径
     * @throws IOException
     */
    public static void copyLargeFile(String source, String dest) throws IOException {
        try (FileChannel in = new FileInputStream(source).getChannel();
             FileChannel out = new FileOutputStream(dest).getChannel()) {
            in.transferTo(0, in.size(), out);
//            out.transferFrom(in, 0, in.size());
        }
    }

    /**
     * 使用缓冲流复制文件到指定路径，适用于复制大文件。
     *
     * @param source 源文件路径
     * @param dest   目标文件路径
     * @throws IOException
     */
    public static void copyLargeFileBuffered(String source, String dest) throws IOException {
        try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(source));
             BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(dest))) {
            inputStream2OutputStream(bis, bos);
        }
    }

    //endregion

    //region read file to bytes

    /**
     * 将文件读取到byte[]。
     *
     * @param path 文件的Path对象
     * @return 返回byte[]
     * @throws IOException
     */
    public static byte[] file2Bytes(Path path) throws IOException {
        return Files.readAllBytes(path);
    }

    /**
     * 将文件读取到byte[]。
     *
     * @param file 文件对象
     * @return 返回byte[]
     * @throws IOException
     */
    public static byte[] file2Bytes(File file) throws IOException {
        return Files.readAllBytes(file.toPath());
    }

    /**
     * 将文件读取到byte[]。
     *
     * @param path 文件路径
     * @return 返回byte[]
     * @throws IOException
     */
    public static byte[] file2Bytes(String path) throws IOException {
//        return Files.readAllBytes(Path.of(path)); // JDK13及以上
        return Files.readAllBytes(Paths.get(path));
    }
    //endregion

    /**
     * 将文件转化为InputStream。
     *
     * @param path 待转化的文件所在的路径
     * @return 返回InputStream
     */
    public static InputStream file2InputStream(String path) throws IOException {
        return file2InputStream(new File(path));
    }

    /**
     * 将文件转化为InputStream。
     *
     * @param file 待转化的文件
     * @return 返回InputStream
     */
    public static InputStream file2InputStream(File file) throws IOException {
        return Files.newInputStream(file.toPath());
    }

    //region IS to OS

    /**
     * 将InputStream写入到OutputStream。
     *
     * @param os 待写入的outputStream
     * @param is 待读取的inputStream
     * @throws IOException
     */
    public static void inputStream2OutputStream(InputStream is, OutputStream os) throws IOException {
        if (is == null) {
            return;
        }
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = is.read(buffer)) != -1) {
            os.write(buffer, 0, len);
        }
    }

    /**
     * 读取InputStream到ByteArrayOutputStream，返回ByteArrayOutputStream。
     *
     * @param is inputStream
     * @return 返回ByteArrayOutputStream
     * @throws IOException
     */
    public static ByteArrayOutputStream inputStream2ByteArrayOutputStream(InputStream is) throws IOException {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        inputStream2OutputStream(is, os);
        return os;
    }

    /**
     * 读取inputStream到指定文件位置。
     *
     * @param inputStream 待读入的inputStream
     * @param path        指定文件位置
     */
    public static void inputStream2File(InputStream inputStream, String path) throws IOException {
        try (FileOutputStream fos = new FileOutputStream(new File(path))) {
            inputStream2OutputStream(inputStream, fos);
        }
    }
    //endregion

    //region RandomAccessFile

    /**
     * 将一个RandomAccessFile写入另一个RandomAccessFile，注意后者需要能写入。
     *
     * @param source 源RandomAccessFile
     * @param dest   目标RandomAccessFile
     */
    public static void raf2Raf(RandomAccessFile source, RandomAccessFile dest) throws IOException {
        byte[] buffer = new byte[1024];
        int len;
        while ((len = source.read(buffer)) != -1) {
            dest.write(buffer, 0, len);
        }
    }

    /**
     * 读取RandomAccessFile内容到StringBuilder。
     *
     * @param raf 待读取的RandomAccessFile
     * @return 返回StringBuilder
     */
    public static StringBuilder raf2StringBuilder(RandomAccessFile raf) throws IOException {
        return raf2StringBuilder(raf, 0);
    }

    /**
     * 读取RandomAccessFile内容到StringBuilder。
     *
     * @param raf 待读取的RandomAccessFile
     * @param pos 开始读取的位置，默认为0，即从头开始读取。
     * @return 返回StringBuilder
     */
    public static StringBuilder raf2StringBuilder(RandomAccessFile raf, long pos) throws IOException {
        // 记录初始指针位置
        long pointer = raf.getFilePointer();
        raf.seek(pos);
        StringBuilder sb = new StringBuilder((int) raf.length());
        byte[] buffer = new byte[1024];
        int len;
        while ((len = raf.read(buffer)) != -1) {
            sb.append(new String(buffer, 0, len));
        }
        // 恢复指针位置
        raf.seek(pointer);
        return sb;
    }

    /**
     * 读取RandomAccessFile内容到StringBuilder。
     *
     * @param raf 待读取的RandomAccessFile
     * @param pos 开始读取的位置，默认为0，即从头开始读取。
     * @return 返回StringBuilder
     */
    public static ByteArrayOutputStream raf2Baos(RandomAccessFile raf, long pos) throws IOException {
        // 记录初始指针位置
        long pointer = raf.getFilePointer();
        raf.seek(pos);
        ByteArrayOutputStream baos = new ByteArrayOutputStream((int) raf.length());
        byte[] buffer = new byte[1024];
        int len;
        while ((len = raf.read(buffer)) != -1) {
            baos.write(buffer, 0, len);
        }
        // 恢复指针位置
        raf.seek(pointer);
        return baos;
    }

    /**
     * 读取RandomAccessFile内容到String。
     *
     * @param raf 待读取的RandomAccessFile
     * @return 返回字符串
     */
    public static String raf2String(RandomAccessFile raf) throws IOException {
        return raf2String(raf, 0);
    }

    /**
     * 读取RandomAccessFile内容到String。
     *
     * @param raf 待读取的RandomAccessFile
     * @param pos 开始读取的位置，默认为0，即从头开始读取。
     * @return 返回字符串
     */
    public static String raf2String(RandomAccessFile raf, long pos) throws IOException {
        return raf2Baos(raf, pos).toString();
    }

    /**
     * 清空RandomAccessFile文件
     *
     * @param raf RandomAccessFile文件
     * @throws IOException 异常
     */
    public static void clearRaf(RandomAccessFile raf) throws IOException {
        raf.setLength(0);
    }

    /**
     * 将字符串写入RandomAccessFile
     *
     * @param str 待写入字符串
     * @param raf RandomAccessFile文件
     */
    public static void string2Raf(String str, RandomAccessFile raf, Charset charset, RafWriteMode mode) throws IOException {
        switch (mode) {
            case Override:
                raf.seek(0);
                break;
            case Erase:
                raf.setLength(0);
                break;
            case Append:
            default:
                raf.seek(raf.length());
                break;
        }
        byte[] buf = str.getBytes(charset);
        raf.write(buf, 0, buf.length);
    }

    /**
     * RandomAccessFile文件的写入模式
     */
    public enum RafWriteMode {
        /**
         * 在文件末尾追加
         */
        Append,

        /**
         * 从头开始覆盖原文件内容
         */
        Override,

        /**
         * 先删除原文件内容，再写入
         */
        Erase,
    }

    //endregion

    //region zip

    /**
     * 将文件（夹）放入zip压缩包
     *
     * @param source 源文件（夹）路径
     * @param dest   目标压缩包路径
     * @throws IOException
     */
    public static void file2Zip(String source, String dest) throws IOException {
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(dest))) {
            File file = new File(source);
            file2Zip(file, zipOut, "");
        }
    }

    /**
     * 将文件（夹）放入zip压缩包，不改变根文件的文件名
     *
     * @param source  源文件（夹）路径
     * @param destDir 目标压缩包目录
     * @throws IOException
     */
    public static void file2Zip_SameName(String source, String destDir) throws IOException {
        file2Zip_SameName(source, destDir, "zip");
    }

    /**
     * 将文件（夹）放入zip压缩包，不改变根文件的文件名
     *
     * @param source  源文件（夹）路径
     * @param destDir 目标压缩包目录
     * @param suffix  压缩文件后缀(不包含".")，默认为“zip"
     * @throws IOException
     */
    public static void file2Zip_SameName(String source, String destDir, String suffix) throws IOException {
        String destName = new File(source).getName();
        String dest = Paths.get(destDir, destName).toString() + "." + suffix;
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(dest))) {
            File file = new File(source);
            file2Zip(file, zipOut, "");
        }
    }

    /**
     * 将文件（夹）放入zip压缩包
     *
     * @param file 文件对象
     * @param dest 目标压缩包路径
     * @throws IOException
     */
    public static void file2Zip(File file, String dest) throws IOException {
        try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(dest))) {
            file2Zip(file, zipOut, "");
        }
    }

    /**
     * 将文件（夹）放入zip压缩包
     *
     * @param file   文件对象
     * @param zipOut ZipOutputStream对象
     * @throws IOException
     */
    private static void file2Zip(File file, ZipOutputStream zipOut) throws IOException {
        file2Zip(file, zipOut, "");
    }

    /**
     * 将文件（夹）放入zip压缩包
     *
     * @param file   文件对象
     * @param zipOut ZipOutputStream对象
     * @param prefix 文件路径前缀，默认为""。
     * @throws IOException
     */
    private static void file2Zip(File file, ZipOutputStream zipOut, String prefix) throws IOException {
        if (file.isDirectory()) {
            // 如果是文件夹
            String entryName = prefix + file.getName() + "/";
            zipOut.putNextEntry(new ZipEntry(entryName));
            File[] files = file.listFiles();
            if (files != null) {
                for (File file1 : files) {
                    file2Zip(file1, zipOut, entryName);
                }
            }
            zipOut.closeEntry();
        } else {
            // 如果是文件
            zipOut.putNextEntry(new ZipEntry(prefix + file.getName()));
            zipOut.write(file2Bytes(file));
            zipOut.closeEntry();
        }
    }

    /**
     * 从压缩文件中读取文件，如果获取失败，返回null。
     *
     * @param zipFileName   压缩文件的名称，绝对路径。
     * @param containedFile 待读取文件的名词，相对路径。
     * @return 如果获取失败，返回null
     */
    public static InputStream getInputStreamFromZipFile(String zipFileName, String containedFile) throws IOException {
        ZipFile zipFile = new ZipFile(zipFileName);
        return getInputStreamFromZipFile(zipFile, containedFile);
    }

    /**
     * 从压缩文件中读取文件，如果获取失败，返回null。
     *
     * @param zipFile       压缩文件。
     * @param containedFile 待读取文件的名词，相对路径。
     * @return 如果获取失败，返回null
     */
    public static InputStream getInputStreamFromZipFile(ZipFile zipFile, String containedFile) throws IOException {
        InputStream is = null;
        ZipEntry entry = zipFile.getEntry(containedFile);
        if (entry != null) {
            is = zipFile.getInputStream(entry);
        }
        return is;
    }

    /**
     * 从压缩文件中解压指定文件。
     *
     * @param zipFile       压缩文件
     * @param containedFile 待解压文件的名词，相对路径。
     * @param pathDest      解压出的文件保存的绝对路径。
     * @throws IOException
     */
    public static void extractZip(ZipFile zipFile, String containedFile, String pathDest) throws IOException {
        InputStream is = getInputStreamFromZipFile(zipFile, containedFile);
        if (is != null) {
            inputStream2File(is, pathDest);
            is.close();
        }
    }

    /**
     * 从压缩文件中解压指定文件。
     *
     * @param zipFilePath       压缩文件的绝对路径。
     * @param containedFile 待解压文件的名词，相对路径。
     * @param pathDest      解压出的文件保存的绝对路径。
     * @throws IOException
     */
    public static void extractZip(String zipFilePath, String containedFile, String pathDest) throws IOException {
        ZipFile zipFile = new ZipFile(zipFilePath);
        InputStream is = getInputStreamFromZipFile(zipFile, containedFile);
        inputStream2File(is, pathDest);
        is.close();
    }
    //endregion


    /**
     * 创建指定的目录，已过时，建议用FsUtils.createDir。
     *
     * @param path 要创建的文件夹地址
     * @return 返回1：成功创建文件夹；-1：文件夹创建失败；0：文件夹已存在
     */
    @Deprecated
    public static Integer createDir(String path) {
        //创建根文件夹
        File folder = new File(path);
        if (!folder.exists() && !folder.isDirectory()) {
            if (folder.mkdirs()) {
                return 1;
            } else {
                return -1;
            }
        } else {
            return 0;
        }
    }

    /**
     * 将字符串写入文件。
     *
     * @param content 待写入的字符串
     * @param path    目标文件的路径
     * @throws IOException
     */
    public static void String2File(String content, String path) throws IOException {
        try (FileOutputStream out = new FileOutputStream(path)) {
            out.write(content.getBytes(StandardCharsets.UTF_8));
        }
    }

    /**
     * 将字符串写入文件。
     *
     * @param content 待写入的字符串
     * @param path    目标文件的路径
     * @throws IOException
     */
    public static void StringBuilder2File(StringBuilder content, String path) throws IOException {
        try (FileOutputStream out = new FileOutputStream(path)) {
            out.write(content.toString().getBytes(StandardCharsets.UTF_8));
        }
    }

    /**
     * 获取在console读入的reader。后续可以调用BufferedReader.ReadLine()方法来读取字符串。
     *
     * @return
     */
    public static BufferedReader getConsoleInput() {
        return new BufferedReader(new InputStreamReader(System.in));
    }

    /**
     * 将对象序列化为baos。
     *
     * @param obj
     * @return
     * @throws IOException
     */
    public static ByteArrayOutputStream serialize(Object obj) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(obj);
        return baos;
    }

    /**
     * 将bais反序列化为object。
     *
     * @param bais
     * @return
     * @throws IOException
     */
    public static Object deserialize(ByteArrayInputStream bais) throws IOException, ClassNotFoundException {
        return new ObjectInputStream(bais).readObject();
    }

    /**
     * 深拷贝对象（注意要先实现Cloneable、Serializable接口）
     *
     * @param obj
     * @return
     * @throws IOException
     * @throws ClassNotFoundException
     */
    public static Object deepClone(Object obj) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream baos = serialize(obj);
        return deserialize(new ByteArrayInputStream(baos.toByteArray()));
    }
}